﻿using System;
using System.Collections.Generic;
using System.IO;

using Microsoft.Xna.Framework;

using AlfarEngine.Components;
using AlfarEngine.Utilities;

namespace AlfarEngine
{
    /// <summary>
    /// The abstract class that define an Object of the Engine. 
    /// Subclass this to allow specification (redefinition of Update and Draw)
    /// </summary>
    public class GameObject
    {
        #region Members

        //Private because derivated classes shouldn't touch them diretcly, but use the accessors.
        private Vector2 mUp;
        private Vector2 mRight;

        protected string mName;
        protected Vector2 mPos;

        protected float mRotation;

        protected GameObject mParent;

        protected List<GameObject> mChildren;
        protected List<AComponent> mComponents;

        protected Rectangle mBoundingRect;
        protected Rectangle mWorldBBox;

        protected bool mDirty;

        #endregion

        //------------------------------------------------------------

        #region Accessors

        /// <summary>
        /// The world position of this object
        /// </summary>
        public Vector2 position { get { return mPos; } set { mPos = value; mDirty = true; } }

        public Vector2 screenPosition { get { return position - Camera.mainCamera.upperCorner; } }

        /// <summary>
        /// The name of this object (by default "Object")
        /// </summary>
        public string name { get { return mName; } set { mName = value; } }

        /// <summary>
        /// The parent of this object, null if none.
        /// </summary>
        public GameObject parent { get { return mParent; } set { if (value != null) value.AddObject(this); else mParent = null; } }

        /// <summary>
        /// Return the number of children
        /// </summary>
        public int childrenCount { get { return mChildren.Count; } }

        /// <summary>
        /// The bouding rectangle of this data (never change this manually!)
        /// </summary>
        public Rectangle bounds { get { return mWorldBBox; } set { mWorldBBox = value; } }

        /// <summary>
        /// the bound in local space (relative to it's parent)
        /// </summary>
        public Rectangle localBounds { get { return mBoundingRect; } set { mBoundingRect = value; } }

        /// <summary>
        /// set this GameObject as "dirty", meaning it need to update all it's child, and recompute the bounds of its hierarchy
        /// </summary>
        public bool dirty { get { return mDirty; } set { mDirty = value; } }

        /// <summary>
        /// the rotation of this gameObject. 
        /// </summary>
        public float rotation { get { return mRotation; } set { mRotation = value; ComputeAxis(); } }

        /// <summary>
        /// The up vector. It can be set directly, the right vector and the rotation will be computed accordingly\n
        /// If the vector set is not a normalized one, it will be.
        /// </summary>
        public Vector2 up { get { return mUp; } set { mUp = value; if (Helpers.NearlyEqual(mUp.LengthSquared(), 1.0f, float.Epsilon) ) mUp.Normalize(); ComputeRightFromUp(); } }

        /// <summary>
        /// The right vector. It can be set directly, the up vector and the rotation will be computed accordingly\n
        /// If the vector set is not a normalized one, it will be.
        /// </summary>
        public Vector2 right { get { return mRight; } set { mRight = value; if (Helpers.NearlyEqual(mRight.LengthSquared() , 1.0f, float.Epsilon)) mRight.Normalize(); ComputeUpFromRight(); } }

        #endregion

        //--------------------------------------------------------------

        #region Methods

        /// <summary>
        /// Build an object with the default name "Object"
        /// </summary>
        public GameObject() : this("Object")
        {
        }

        /// <summary>
        /// build a new Object.
        /// </summary>
        /// <param name="pName">The name of this object</param>
        public GameObject(string pName)
        {
            mComponents = new List<AComponent>();
            mChildren = new List<GameObject>();

            mParent = null;

            mName = pName;
            mPos = Vector2.Zero;

            mBoundingRect = Rectangle.Empty;

            mDirty = true;

            mRotation = 0;

            mUp = -Vector2.UnitY;
            mRight = Vector2.UnitX;
        }

        /// <summary>
        /// Update this object (call update on all components)
        /// </summary>
        /// <param name="pTime"></param>
        public virtual void Update(GameTime pTime)
        {
            for (int i = 0; i < mChildren.Count; i++)
            {
                mChildren[i].Update(pTime);
            }

            for (int i = 0; i < mComponents.Count; i++)
            {
                mComponents[i].Update(pTime);
            }

            if (mDirty)
            {
                UpdateBoudingRectUpward();
                mDirty = false;
            }
        }

        /// <summary>
        /// Draw this object (call draw on all components)
        /// </summary>
        /// <param name="pTime"></param>
        public virtual void Draw(GameTime pTime)
        {
            Draw(pTime, Color.White);
        }

        public virtual void Draw(GameTime pTime, Color pColor)
        {
            //Check against Current Viewport before drawing

            if (!IsVisible())
                return;

            for (int i = 0; i < mChildren.Count; i++)
            {
                mChildren[i].Draw(pTime, pColor);
            }

            for (int i = 0; i < mComponents.Count; i++)
            {
                mComponents[i].Draw(pTime, pColor);
            }

            //Utilities.Debug.DrawRectangle(mWorldBBox);
        }

        /// <summary>
        /// Add an Object as Child of this Object
        /// </summary>
        /// <param name="pChild"></param>
        public void AddObject(GameObject pChild)
        {
            mChildren.Add(pChild);
            pChild.mParent = this;
        }

        /// <summary>
        /// Add a component of type T
        /// </summary>
        /// <typeparam name="T">The Type of the Component to Add. Must be derived from AComponent</typeparam>
        /// <returns></returns>
        public T AddComponent<T>() where T : AComponent
        {
            object[] lObj = { this };
            T lInst = Activator.CreateInstance(typeof(T), lObj) as T;

            mComponents.Add(lInst as AComponent);

            return lInst;
        }

        /// <summary>
        /// Return the first component of type T it find. if none are found, return null
        /// </summary>
        /// <typeparam name="T">The type you want to get</typeparam>
        /// <returns>the component if found, null if not</returns>
        public T GetComponent<T>() where T : AComponent
        {
            for (int i = 0; i < mComponents.Count; i++)
            {
                if (mComponents[i].GetType() == typeof(T))
                {
                    return mComponents[i] as T;
                }
            }

            return null;
        }

        /// <summary>
        /// Build the boundingRect upward. Call this for example at the start of the level if you're bulding the level proceduraly.
        /// It will allow to optimize the drawing.
        /// </summary>
        public void BuildBoundingRect()
        {
            if(mChildren.Count == 0)
                mWorldBBox = new Rectangle((int)(position.X + localBounds.X), (int)(position.Y + localBounds.Y), localBounds.Width, localBounds.Height);

            for (int i = 0; i < mChildren.Count; i++)
            {
                mChildren[i].BuildBoundingRect();

                mWorldBBox = Rectangle.Union(mWorldBBox, mChildren[i].mWorldBBox);
            }
        }

        public void UpdateBoudingRectUpward()
        {
            for (int i = 0; i < mChildren.Count; i++)
            {
                mWorldBBox = Rectangle.Union(mWorldBBox, mChildren[i].mWorldBBox);
            }

            if (parent != null)
                parent.UpdateBoudingRectUpward();
        }


        /// <summary>
        /// Check if this gameobject is visible in the current viewport (Engine.currentViewport).
        /// </summary>
        /// <returns>true if inside the viewport, false otherwise</returns>
        public bool IsVisible()
        {
            return mWorldBBox.Intersects(Engine.currentViewport);
        }

        public GameObject GetChild(int i)
        {
            if (i > mChildren.Count)
                return null;

            return mChildren[i];
        }

        /// <summary>
        /// Serialize as XML this object (and it's children and components) to a given TextWriter
        /// </summary>
        /// <param name="pFile"></param>
        public void Serialize(BinaryWriter pFile)
        {
            pFile.Write(position.X);
            pFile.Write(position.Y);

            pFile.Write(rotation);

            pFile.Write(mComponents.Count);
            for (int i = 0; i < mComponents.Count; i++)
            {
                mComponents[i].Serialize(pFile);
            }

            pFile.Write(mChildren.Count);
            for (int i = 0; i < mChildren.Count; i++)
            {
                mChildren[i].Serialize(pFile);
            }
        }

        public void Deserialize(BinaryReader pFile)
        {
            position = new Vector2(pFile.ReadSingle(), pFile.ReadSingle()) ;
            rotation = pFile.ReadSingle();

            int lComponentsCount = pFile.ReadInt32();
            for (int i = 0; i < lComponentsCount; i++)
            {
                string lType = pFile.ReadString();

                if(lType == typeof(Sprite).ToString())
                {
                    Sprite lSpr = AddComponent<Sprite>();
                    lSpr.Deserialize(pFile);
                }
                else if(lType == typeof(AnimatedSprite).ToString())
                {
                    AnimatedSprite lSpr = AddComponent<AnimatedSprite>();
                    lSpr.Deserialize(pFile);
                }
                else if(lType == typeof(Collider).ToString())
                {
                    Collider lCol = AddComponent<Collider>();
                    lCol.Deserialize(pFile);
                }
            }

            int lGameObjectCount = pFile.ReadInt32();
            for (int i = 0; i < lGameObjectCount; i++)
            {
                GameObject lObj = new GameObject();
                lObj.Deserialize(pFile);
                AddObject(lObj);
            }
        }

        //***----------- PRIVATE METHODS -----------***//

        // Recompute the Up and Right axis.
        private void ComputeAxis()
        {
            mUp     = Vector2.Transform(-Vector2.UnitY, Matrix.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(mRotation)));
            mRight  = Vector2.Transform( Vector2.UnitX, Matrix.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(mRotation)));
        }

        private void ComputeUpFromRight()
        {
            mUp = Vector2.Transform(mRight, Matrix.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(-90.0f)));

            mRotation = MathHelper.ToDegrees((float)(Math.Atan2(0, -1) - Math.Atan2(mUp.X, mUp.Y)));
        }

        private void ComputeRightFromUp()
        {
            mRight = Vector2.Transform(mUp, Matrix.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(90.0f)));

            mRotation = MathHelper.ToDegrees((float)(Math.Atan2(0, -1) - Math.Atan2(mUp.X, mUp.Y)));
        }

        #endregion
    }
}
